diff --git a/docs/rules/README.md b/docs/rules/README.md
index 860b0f731..4239d3a21 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -287,14 +287,15 @@ 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-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | |
| [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 | |
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
-| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
+| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |
diff --git a/docs/rules/no-restricted-static-attribute.md b/docs/rules/no-restricted-static-attribute.md
new file mode 100644
index 000000000..a2009d7a1
--- /dev/null
+++ b/docs/rules/no-restricted-static-attribute.md
@@ -0,0 +1,97 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-static-attribute
+description: disallow specific attribute
+---
+# vue/no-restricted-static-attribute
+> disallow specific attribute
+
+## :book: Rule Details
+
+This rule allows you to specify attribute 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 attribute name or pattern to be restricted:
+
+```json
+{
+ "vue/no-restricted-static-attribute": ["error", "foo", "bar"]
+}
+```
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+ "vue/no-restricted-static-attribute": ["error",
+ {
+ "key": "stlye",
+ "message": "Using \"stlye\" is not allowed. Use \"style\" instead."
+ }
+ ]
+}
+```
+
+The following properties can be specified for the object.
+
+- `key` ... Specify the attribute key name or pattern.
+- `value` ... Specify the value text or pattern or `true`. If specified, it will only be reported if the specified value is used. If `true`, it will only be reported if there is no value or if the value and key are same.
+- `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.
+
+### `{ "key": "foo", "value": "bar" }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+### `{ "key": "foo", "element": "MyButton" }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+## :couple: Related rules
+
+- [vue/no-restricted-v-bind]
+
+[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-static-attribute.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-static-attribute.js)
diff --git a/lib/index.js b/lib/index.js
index 974ea15e0..45d0d58fe 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -80,6 +80,7 @@ module.exports = {
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
+ 'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
'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'),
diff --git a/lib/rules/no-restricted-static-attribute.js b/lib/rules/no-restricted-static-attribute.js
new file mode 100644
index 000000000..70db824db
--- /dev/null
+++ b/lib/rules/no-restricted-static-attribute.js
@@ -0,0 +1,166 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute
+ */
+/**
+ * @typedef {object} ParsedOption
+ * @property { (key: VAttribute) => boolean } test
+ * @property {boolean} [useValue]
+ * @property {boolean} [useElement]
+ * @property {string} [message]
+ */
+
+/**
+ * @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 matcher(key.rawName)
+ }
+ }
+ }
+ const parsed = parseOption(option.key)
+ if (option.value) {
+ const keyTest = parsed.test
+ if (option.value === true) {
+ parsed.test = (node) => {
+ if (!keyTest(node)) {
+ return false
+ }
+ return node.value == null || node.value.value === node.key.rawName
+ }
+ } else {
+ const valueMatcher = buildMatcher(option.value)
+ parsed.test = (node) => {
+ if (!keyTest(node)) {
+ return false
+ }
+ return node.value != null && valueMatcher(node.value.value)
+ }
+ }
+ parsed.useValue = true
+ }
+ if (option.element) {
+ const argTest = parsed.test
+ const tagMatcher = buildMatcher(option.element)
+ parsed.test = (node) => {
+ if (!argTest(node)) {
+ return false
+ }
+ const element = node.parent.parent
+ return tagMatcher(element.rawName)
+ }
+ parsed.useElement = true
+ }
+ parsed.message = option.message
+ return parsed
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow specific attribute',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/no-restricted-static-attribute.html'
+ },
+ fixable: null,
+ schema: {
+ type: 'array',
+ items: {
+ oneOf: [
+ { type: 'string' },
+ {
+ type: 'object',
+ properties: {
+ key: { type: 'string' },
+ value: { anyOf: [{ type: 'string' }, { enum: [true] }] },
+ element: { type: 'string' },
+ message: { type: 'string', minLength: 1 }
+ },
+ required: ['key'],
+ additionalProperties: false
+ }
+ ]
+ },
+ uniqueItems: true,
+ minItems: 0
+ },
+
+ messages: {
+ // eslint-disable-next-line eslint-plugin/report-message-format
+ restrictedAttr: '{{message}}'
+ }
+ },
+ create(context) {
+ if (!context.options.length) {
+ return {}
+ }
+ /** @type {ParsedOption[]} */
+ const options = context.options.map(parseOption)
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /**
+ * @param {VAttribute} node
+ */
+ 'VAttribute[directive=false]'(node) {
+ for (const option of options) {
+ if (option.test(node)) {
+ const message = option.message || defaultMessage(node, option)
+ context.report({
+ node,
+ messageId: 'restrictedAttr',
+ data: { message }
+ })
+ return
+ }
+ }
+ }
+ })
+
+ /**
+ * @param {VAttribute} node
+ * @param {ParsedOption} option
+ */
+ function defaultMessage(node, option) {
+ const key = node.key.rawName
+ const value = !option.useValue
+ ? ''
+ : node.value == null
+ ? '` set to `true'
+ : `="${node.value.value}"`
+
+ let on = ''
+ if (option.useElement) {
+ on = ` on \`<${node.parent.parent.rawName}>\``
+ }
+ return `Using \`${key + value}\`${on} is not allowed.`
+ }
+ }
+}
diff --git a/tests/lib/rules/no-restricted-static-attribute.js b/tests/lib/rules/no-restricted-static-attribute.js
new file mode 100644
index 000000000..1b57e9145
--- /dev/null
+++ b/tests/lib/rules/no-restricted-static-attribute.js
@@ -0,0 +1,151 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-static-attribute')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('no-restricted-static-attribute', rule, {
+ valid: [
+ {
+ 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: [{ key: 'foo', value: 'bar' }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ key: 'foo', element: 'input' }]
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['foo'],
+ errors: [
+ {
+ message: 'Using `foo` is not allowed.',
+ line: 1,
+ column: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['foo'],
+ errors: ['Using `foo` is not allowed.']
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['/^f/'],
+ 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: [{ key: '/^(foo|bar)$/' }],
+ errors: ['Using `foo` is not allowed.', 'Using `bar` is not allowed.']
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ key: 'foo', value: 'bar' }],
+ errors: ['Using `foo="bar"` is not allowed.']
+ },
+ {
+ filename: 'test.vue',
+ code:
+ '',
+ options: [
+ '/^vv/',
+ { key: 'foo', value: true },
+ { key: 'bar', value: '/^vv/' }
+ ],
+ errors: [
+ 'Using `foo` set to `true` is not allowed.',
+ 'Using `foo="foo"` is not allowed.',
+ 'Using `vv` is not allowed.',
+ 'Using `vvv` is not allowed.',
+ 'Using `bar="vv"` is not allowed.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: [{ key: 'foo', element: `/^My/` }],
+ errors: ['Using `foo` on `` is not allowed.']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: ['/^f/', { key: 'foo' }],
+ errors: ['Using `foo` is not allowed.']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: [{ key: 'foo', message: 'foo' }],
+ errors: ['foo']
+ }
+ ]
+})