Repository: vuejs/eslint-plugin-vue (js)

Issue:
Disallow misspellings of properties (stlye, clsas)
**Please describe what the rule should do:**
The rule should disallow the usage of misspellings of `style` and `class` (such as stlye) in the template, as misspelling them can waste a lot of time while trying to figure it out.

Should I make this rule accept a list of blacklisted property names, and have the misspellings as the default? Or should I keep it specific to the misspellings of `style` and `class`, and allow the user to give it a whitelist of words from that list that they want to allow?

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
- [ ] Enforces code style
- [X] Warns about a potential error
- [ ] Suggests an alternate way of doing something
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**
```vue
<div stlye="display: none;"></div>
```

Pull request description:
Add `vue/no-restricted-static-attribute` rule
This PR adds `vue/no-restricted-static-attribute` rule that disallow specific attribute.

The following options can be used to report typos such as `stlye`.

```json
{
  "vue/no-restricted-static-attribute": ["error", "stlye"]
}
```

We can close the issue #992 by combining it with the `vue/no-restricted-v-bind` rule added in #1191.

Close #992.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 860b0f73..4239d3a2 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 00000000..a2009d7a
--- /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"]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', 'foo', 'bar']}">
+
+```vue
+<template>
+  <!-- ✘ BAD -->
+  <div foo="x" />
+  <div bar />
+</template>
+```
+
+</eslint-code-block>
+
+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"  }`
+
+<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', { key: 'foo', value: 'bar' }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div foo="foo" />
+
+  <!-- ✘ BAD -->
+  <div foo="bar" />
+</template>
+```
+
+</eslint-code-block>
+
+### `{ "key": "foo", "element": "MyButton"  }`
+
+<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', { key: 'foo', element: 'MyButton' }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <CoolButton foo="x" />
+
+  <!-- ✘ BAD -->
+  <MyButton foo="x" />
+</template>
+```
+
+</eslint-code-block>
+
+## :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 974ea15e..45d0d58f 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 00000000..70db824d
--- /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.`
+    }
+  }
+}


Test patch:
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 00000000..1b57e914
--- /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: '<template><div foo="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div :foo="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-model="foo"></div></template>',
+      options: ['foo']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div bar="foo"></div></template>',
+      options: ['foo']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div :foo="foo"></div></template>',
+      options: ['foo']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="foo"></div></template>',
+      options: [{ key: 'foo', value: 'bar' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="foo"></div><input bar></template>',
+      options: [{ key: 'foo', element: 'input' }]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="foo"></div></template>',
+      options: ['foo'],
+      errors: [
+        {
+          message: 'Using `foo` is not allowed.',
+          line: 1,
+          column: 16
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="bar" bar="foo"></div></template>',
+      options: ['foo'],
+      errors: ['Using `foo` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="foo" bar="bar"></div></template>',
+      options: ['/^f/'],
+      errors: ['Using `foo` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="bar" bar="foo"></div></template>',
+      options: ['foo', 'bar'],
+      errors: ['Using `foo` is not allowed.', 'Using `bar` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="bar" bar></div></template>',
+      options: [{ key: '/^(foo|bar)$/' }],
+      errors: ['Using `foo` is not allowed.', 'Using `bar` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div foo="foo" /><div foo="bar" /></template>',
+      options: [{ key: 'foo', value: 'bar' }],
+      errors: ['Using `foo="bar"` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div foo v bar /><div foo="foo" vv="foo" bar="vfoo" /><div vvv="foo" bar="vv" /></template>',
+      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: `
+      <template>
+        <div foo />
+        <MyButton foo bar />
+      </template>`,
+      options: [{ key: 'foo', element: `/^My/` }],
+      errors: ['Using `foo` on `<MyButton>` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div foo="x" />
+      </template>`,
+      options: ['/^f/', { key: 'foo' }],
+      errors: ['Using `foo` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div foo="x" />
+      </template>`,
+      options: [{ key: 'foo', message: 'foo' }],
+      errors: ['foo']
+    }
+  ]
+})