Repository: vuejs/eslint-plugin-vue
Pull request #1193 (2020-06-06 12:52:34)

Add `vue/object-property-newline` rule
This PR adds the wrapper rule of the [object-property-newline](https://eslint.org/docs/rules/object-property-newline) core rule to apply to the expressions in `<template>`.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e44b2855..1e299adf 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -287,13 +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()` |  |
@@ -329,6 +331,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
 | [vue/no-useless-concat](./no-useless-concat.md) | disallow unnecessary concatenation of literals or template literals |  |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
+| [vue/object-property-newline](./object-property-newline.md) | enforce placing object properties on separate lines | :wrench: |
 | [vue/prefer-template](./prefer-template.md) | require template literals instead of string concatenation | :wrench: |
 | [vue/space-in-parens](./space-in-parens.md) | enforce consistent spacing inside parentheses | :wrench: |
 | [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators | :wrench: |
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/docs/rules/no-restricted-v-bind.md b/docs/rules/no-restricted-v-bind.md
new file mode 100644
index 00000000..cc141f1b
--- /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"]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-v-bind': ['error', '/^v-/', 'foo', 'bar']}">
+
+```vue
+<template>
+  <!-- ✘ BAD -->
+  <div v-bind:foo="x" />
+  <div :bar="x" />
+</template>
+```
+
+</eslint-code-block>
+
+By default, `'/^v-/'` is set. This prevents mistakes intended to be directives.
+
+<eslint-code-block :rules="{'vue/no-restricted-v-bind': ['error']}">
+
+```vue
+<template>
+  <!-- ✘ BAD -->
+  <MyInput :v-model="x" />
+  <div :v-if="x" />
+</template>
+```
+
+</eslint-code-block>
+
+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"]  }`
+
+<eslint-code-block :rules="{'vue/no-restricted-v-bind': ['error', { argument: 'foo', modifiers: ['prop'] }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div :foo="x" />
+
+  <!-- ✘ BAD -->
+  <div :foo.prop="x" />
+</template>
+```
+
+</eslint-code-block>
+
+### `{ "argument": "foo", "element": "MyButton"  }`
+
+<eslint-code-block :rules="{'vue/no-restricted-v-bind': ['error', { argument: 'foo', element: 'MyButton' }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <CoolButton :foo="x" />
+
+  <!-- ✘ BAD -->
+  <MyButton :foo="x" />
+</template>
+```
+
+</eslint-code-block>
+
+## :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/docs/rules/object-property-newline.md b/docs/rules/object-property-newline.md
new file mode 100644
index 00000000..3095ebfc
--- /dev/null
+++ b/docs/rules/object-property-newline.md
@@ -0,0 +1,25 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/object-property-newline
+description: enforce placing object properties on separate lines
+---
+# vue/object-property-newline
+> enforce placing object properties on separate lines
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+This rule is the same rule as core [object-property-newline] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [object-property-newline]
+
+[object-property-newline]: https://eslint.org/docs/rules/object-property-newline
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/object-property-newline.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/object-property-newline.js)
+
+<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/object-property-newline)</sup>
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index e3b3fd9b..49413285 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -31,6 +31,7 @@ module.exports = {
     'vue/no-multi-spaces': 'off',
     'vue/no-spaces-around-equal-signs-in-attribute': 'off',
     'vue/object-curly-spacing': 'off',
+    'vue/object-property-newline': 'off',
     'vue/padding-line-between-blocks': 'off',
     'vue/script-indent': 'off',
     'vue/singleline-html-element-content-newline': 'off',
diff --git a/lib/index.js b/lib/index.js
index 04b4af11..2b5169b5 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -80,7 +80,9 @@ 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'),
     'no-shared-component-data': require('./rules/no-shared-component-data'),
     'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
@@ -103,6 +105,7 @@ module.exports = {
     'no-v-model-argument': require('./rules/no-v-model-argument'),
     'no-watch-after-await': require('./rules/no-watch-after-await'),
     'object-curly-spacing': require('./rules/object-curly-spacing'),
+    'object-property-newline': require('./rules/object-property-newline'),
     'one-component-per-file': require('./rules/one-component-per-file'),
     'order-in-components': require('./rules/order-in-components'),
     'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
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.`
+    }
+  }
+}
diff --git a/lib/rules/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js
new file mode 100644
index 00000000..a87be4d2
--- /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/lib/rules/object-property-newline.js b/lib/rules/object-property-newline.js
new file mode 100644
index 00000000..d58aeb84
--- /dev/null
+++ b/lib/rules/object-property-newline.js
@@ -0,0 +1,12 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
+module.exports = wrapCoreRule(
+  require('eslint/lib/rules/object-property-newline'),
+  { skipDynamicArguments: true }
+)


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']
+    }
+  ]
+})
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 00000000..6a7f0a44
--- /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: '<template><div v-bind="foo"></div></template>'
+    },
+    {
+      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: [{ argument: 'foo', modifiers: ['sync'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div :foo="foo"></div></template>',
+      options: [{ argument: 'foo', element: 'input' }]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div :v-model="foo"></div></template>',
+      errors: [
+        {
+          message:
+            'Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive.',
+          line: 1,
+          column: 16
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-bind:v-model="foo"></div></template>',
+      errors: [
+        'Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive.'
+      ]
+    },
+    {
+      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="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.sync="foo"></div></template>',
+      options: [{ argument: '/^(foo|bar)$/' }],
+      errors: ['Using `:foo` is not allowed.', 'Using `:bar` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div :foo.sync="foo" /><div :foo="foo" /></template>',
+      options: [{ argument: 'foo', modifiers: ['sync'] }],
+      errors: ['Using `:foo.sync` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div :v-on :foo.sync /><div :foo="foo" v-bind="listener" /></template>',
+      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: `
+      <template>
+        <div :v-on :foo.sync />
+        <MyButton :foo="foo" :bar="bar" />
+      </template>`,
+      options: ['/^v-/', { argument: 'foo', element: `/^My/` }],
+      errors: [
+        'Using `:v-on` is not allowed.',
+        'Using `:foo` on `<MyButton>` is not allowed.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div :foo="x" />
+      </template>`,
+      options: ['/^f/', { argument: 'foo' }],
+      errors: ['Using `:foo` is not allowed.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div :foo="x" />
+      </template>`,
+      options: [{ argument: 'foo', message: 'foo' }],
+      errors: ['foo']
+    }
+  ]
+})
diff --git a/tests/lib/rules/object-property-newline.js b/tests/lib/rules/object-property-newline.js
new file mode 100644
index 00000000..2de002ca
--- /dev/null
+++ b/tests/lib/rules/object-property-newline.js
@@ -0,0 +1,90 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/object-property-newline')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('object-property-newline', rule, {
+  valid: [
+    `
+    <template>
+      <div :foo="{a: 1,
+        b: [2, {a: 3,
+          b: 4}]}" />
+    </template>
+    `,
+    `
+    <template>
+      <div :foo="{a: 1,
+        b: 2}" />
+    </template>
+    `,
+    `
+    <template>
+      <div :[{a:1,b:2}]="value" />
+    </template>
+    `,
+    {
+      code: `
+      <template>
+        <div :foo="{a: 1, b: [2, {a: 3, b: 4}]}" />
+      </template>
+      `,
+      options: [{ allowAllPropertiesOnSameLine: true }]
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div :foo="{a: 1, b: [2, {a: 3, b: 4}]}" />
+      </template>
+      `,
+      output: `
+      <template>
+        <div :foo="{a: 1,
+b: [2, {a: 3,
+b: 4}]}" />
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Object properties must go on a new line.',
+          line: 3,
+          column: 27
+        },
+        {
+          message: 'Object properties must go on a new line.',
+          line: 3,
+          column: 41
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :foo="{a: 1, b: 2,
+          c: 3}" />
+      </template>
+      `,
+      output: `
+      <template>
+        <div :foo="{a: 1,
+b: 2,
+          c: 3}" />
+      </template>
+      `,
+      options: [{ allowAllPropertiesOnSameLine: true }],
+      errors: [
+        "Object properties must go on a new line if they aren't all on the same line."
+      ]
+    }
+  ]
+})