Repository: vuejs/eslint-plugin-vue
Pull request #1213 (2020-06-15 07:20:22)

Add `vue/no-restricted-component-options` rule
This PR adds `vue/no-restricted-component-options` rule.

The `vue/no-restricted-component-options` rule allows you to specify component options that you don't want to use in your application.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 1fe65868..fa734f4c 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -42,6 +42,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher |  |
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  |
 | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: |
+| [vue/no-deprecated-destroyed-lifecycle](./no-deprecated-destroyed-lifecycle.md) | disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-dollar-listeners-api](./no-deprecated-dollar-listeners-api.md) | disallow using deprecated `$listeners` (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-dollar-scopedslots-api](./no-deprecated-dollar-scopedslots-api.md) | disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+) | :wrench: |
 | [vue/no-deprecated-events-api](./no-deprecated-events-api.md) | disallow using deprecated events api (in Vue.js 3.0.0+) |  |
@@ -287,6 +288,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-component-options](./no-restricted-component-options.md) | disallow specific component option |  |
 | [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 |  |
diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md
index 40041119..2edbfda6 100644
--- a/docs/rules/no-bare-strings-in-template.md
+++ b/docs/rules/no-bare-strings-in-template.md
@@ -57,7 +57,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind]
 ```js
 {
   "vue/no-bare-strings-in-template": ["error", {
-    "whitelist": [
+    "allowlist": [
       "(", ")", ",", ".", "&", "+", "-", "=", "*", "/", "#", "%", "!", "?", ":", "[", "]", "{", "}", "<", ">", "\u00b7", "\u2022", "\u2010", "\u2013", "\u2014", "\u2212", "|"
     ],
     "attributes": {
@@ -70,7 +70,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind]
 }
 ```
 
-- `whitelist` ... An array of whitelisted strings.
+- `allowlist` ... An array of allowed strings.
 - `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name.
 - `directives` ... An array of directive names to check literal value.
 
diff --git a/docs/rules/no-deprecated-destroyed-lifecycle.md b/docs/rules/no-deprecated-destroyed-lifecycle.md
new file mode 100644
index 00000000..f7f7cd8d
--- /dev/null
+++ b/docs/rules/no-deprecated-destroyed-lifecycle.md
@@ -0,0 +1,43 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-destroyed-lifecycle
+description: disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)
+---
+# vue/no-deprecated-destroyed-lifecycle
+> disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+
+## :book: Rule Details
+
+This rule reports use of deprecated `destroyed` and `beforeDestroy` lifecycle hooks. (in Vue.js 3.0.0+).
+
+<eslint-code-block :rules="{'vue/no-deprecated-destroyed-lifecycle': ['error']}">
+
+```vue
+<script>
+export default {
+  /* ✓ GOOD */
+  beforeMount () {},
+  mounted () {},
+  beforeUnmount () {},
+  unmounted () {},
+
+  /* ✗ BAD */
+  beforeDestroy () {},
+  destroyed () {}
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-destroyed-lifecycle.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-destroyed-lifecycle.js)
diff --git a/docs/rules/no-restricted-component-options.md b/docs/rules/no-restricted-component-options.md
new file mode 100644
index 00000000..41292bc6
--- /dev/null
+++ b/docs/rules/no-restricted-component-options.md
@@ -0,0 +1,124 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-component-options
+description: disallow specific component option
+---
+# vue/no-restricted-component-options
+> disallow specific component option
+
+## :book: Rule Details
+
+This rule allows you to specify component options that you don't want to use in your application.
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is a component option name or pattern to be restricted:
+
+```json
+{
+  "vue/no-restricted-component-options": ["error", "init", "beforeCompile", "compiled", "activate", "ready", "/^(?:at|de)tached$/"]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error', 'init', 'beforeCompile', 'compiled', 'activate', 'ready', '/^(?:at|de)tached$/']}">
+
+```vue
+<script>
+export default {
+  /* ✗ BAD */
+  init: function () {},
+  beforeCompile: function () {},
+  compiled: function () {},
+  activate: function () {},
+  ready: function () {},
+  attached: function () {},
+  detached: function () {},
+
+  /* ✓ GOOD */
+  beforeCreate: function () {},
+  activated: function () {},
+  mounted: function () {},
+}
+</script>
+```
+
+</eslint-code-block>
+
+Also, you can use an array to specify the path of object properties.
+
+e.g. `[ "error", ["props", "/.*/", "twoWay"] ]`
+
+<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error' , ['props', '/.*/', 'twoWay'] ]}">
+
+```vue
+<script>
+export default {
+  props: {
+    size: Number,
+    name: {
+      type: String,
+      required: true,
+      /* ✗ BAD */
+      twoWay: true
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+You can use `"*"` to match all properties, including computed keys.
+
+e.g. `[ "error", ["props", "*", "twoWay"] ]`
+
+<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error' , ['props', '*', 'twoWay'] ]}">
+
+```vue
+<script>
+export default {
+  props: {
+    [foo + bar]: {
+      type: String,
+      required: true,
+      /* ✗ BAD */
+      twoWay: true
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+  "vue/no-restricted-component-options": ["error",
+    {
+      "name": "init",
+      "message": "Use \"beforeCreate\" instead."
+    },
+    {
+      "name": "/^(?:at|de)tached$/",
+      "message": "\"attached\" and \"detached\" is deprecated."
+    },
+    {
+      "name": ["props", "/.*/", "twoWay"],
+      "message": "\"props.*.twoWay\" cannot be used."
+    }
+  ]
+}
+```
+
+The following properties can be specified for the object.
+
+- `name` ... Specify the component option name or pattern, or the path by its array.
+- `message` ... Specify an optional custom message.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-component-options.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-component-options.js)
diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md
index 5bb39148..a9a8693c 100644
--- a/docs/rules/no-unsupported-features.md
+++ b/docs/rules/no-unsupported-features.md
@@ -27,6 +27,9 @@ This rule reports unsupported Vue.js syntax on the specified version.
 - `version` ... The `version` option accepts [the valid version range of `node-semver`](https://github.com/npm/node-semver#range-grammar). Set the version of Vue.js you are using. This option is required.
 - `ignores` ... You can use this `ignores` option to ignore the given features.
 The `"ignores"` option accepts an array of the following strings.
+  - Vue.js 3.0.0+
+    - `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
+    - `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
   - Vue.js 2.6.0+
     - `"dynamic-directive-arguments"` ... [dynamic directive arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments).
     - `"v-slot"` ... [v-slot](https://vuejs.org/v2/api/#v-slot) directive.
@@ -35,6 +38,25 @@ The `"ignores"` option accepts an array of the following strings.
   - Vue.js `">=2.6.0-beta.1 <=2.6.0-beta.3"` or 2.6 custom build
     - `"v-bind-prop-modifier-shorthand"` ... [v-bind](https://vuejs.org/v2/api/#v-bind) with `.prop` modifier shorthand.
 
+### `{"version": "^2.6.0"}`
+
+<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.6.0'}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyInput v-bind:foo.sync="val" />
+
+  <!-- ✗ BAD -->
+  <!-- argument on `v-model` -->
+  <MyInput v-model:foo="val" />
+  <!-- custom modifiers on `v-model` -->
+  <MyComp v-model.foo.bar="text" />
+</template>
+```
+
+</eslint-code-block>
+
 ### `{"version": "^2.5.0"}`
 
 <eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.5.0'}]}">
@@ -71,10 +93,20 @@ The `"ignores"` option accepts an array of the following strings.
 - [Guide - Dynamic Arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments)
 - [API - v-slot](https://vuejs.org/v2/api/#v-slot)
 - [API - slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated)
-- [Vue RFCs - 0001-new-slot-syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md)
-- [Vue RFCs - 0002-slot-syntax-shorthand](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md)
-- [Vue RFCs - 0003-dynamic-directive-arguments](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0003-dynamic-directive-arguments.md)
-- [Vue RFCs - v-bind .prop shorthand proposal](https://github.com/vuejs/rfcs/pull/18)
+- [Vue RFCs - 0001-new-slot-syntax]
+- [Vue RFCs - 0002-slot-syntax-shorthand]
+- [Vue RFCs - 0003-dynamic-directive-arguments]
+- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
+- [Vue RFCs - 0011-v-model-api-change]
+- [Vue RFCs - v-bind .prop shorthand proposal]
+
+[Vue RFCs - 0001-new-slot-syntax]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
+[Vue RFCs - 0002-slot-syntax-shorthand]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md
+[Vue RFCs - 0003-dynamic-directive-arguments]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0003-dynamic-directive-arguments.md
+[Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md
+[Vue RFCs - 0011-v-model-api-change]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md
+
+[Vue RFCs - v-bind .prop shorthand proposal]: https://github.com/vuejs/rfcs/pull/18
 
 ## :mag: Implementation
 
diff --git a/docs/rules/require-prop-type-constructor.md b/docs/rules/require-prop-type-constructor.md
index af004aaf..9d48c071 100644
--- a/docs/rules/require-prop-type-constructor.md
+++ b/docs/rules/require-prop-type-constructor.md
@@ -14,7 +14,7 @@ description: require prop type to be a constructor
 
 This rule reports prop types that can't be presumed as constructors.
 
-It's impossible to catch every possible case and know whether the prop type is a constructor or not, hence this rule black list few types of nodes, instead of white-listing correct ones.
+It's impossible to catch every possible case and know whether the prop type is a constructor or not, hence this rule restricts few types of nodes, instead of allowing correct ones.
 
 The following types are forbidden and will be reported:
 
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index b802623d..432df355 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -10,6 +10,7 @@ module.exports = {
     'vue/no-arrow-functions-in-watch': 'error',
     'vue/no-async-in-computed-properties': 'error',
     'vue/no-deprecated-data-object-declaration': 'error',
+    'vue/no-deprecated-destroyed-lifecycle': 'error',
     'vue/no-deprecated-dollar-listeners-api': 'error',
     'vue/no-deprecated-dollar-scopedslots-api': 'error',
     'vue/no-deprecated-events-api': 'error',
diff --git a/lib/index.js b/lib/index.js
index f962015c..81f7dfc7 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -51,6 +51,7 @@ module.exports = {
     'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
     'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
     'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
+    'no-deprecated-destroyed-lifecycle': require('./rules/no-deprecated-destroyed-lifecycle'),
     'no-deprecated-dollar-listeners-api': require('./rules/no-deprecated-dollar-listeners-api'),
     'no-deprecated-dollar-scopedslots-api': require('./rules/no-deprecated-dollar-scopedslots-api'),
     'no-deprecated-events-api': require('./rules/no-deprecated-events-api'),
@@ -81,6 +82,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-component-options': require('./rules/no-restricted-component-options'),
     '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'),
diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js
index d0426e3a..29bc17db 100644
--- a/lib/rules/no-bare-strings-in-template.js
+++ b/lib/rules/no-bare-strings-in-template.js
@@ -21,7 +21,7 @@ const casing = require('../utils/casing')
 // ------------------------------------------------------------------------------
 
 // https://dev.w3.org/html5/html-author/charref
-const DEFAULT_WHITELIST = [
+const DEFAULT_ALLOWLIST = [
   '(',
   ')',
   ',',
@@ -128,7 +128,7 @@ module.exports = {
       {
         type: 'object',
         properties: {
-          whitelist: {
+          allowlist: {
             type: 'array',
             items: { type: 'string' },
             uniqueItems: true
@@ -164,12 +164,12 @@ module.exports = {
      */
     const opts = context.options[0] || {}
     /** @type {string[]} */
-    const whitelist = opts.whitelist || DEFAULT_WHITELIST
+    const allowlist = opts.allowlist || DEFAULT_ALLOWLIST
     const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
     const directives = opts.directives || DEFAULT_DIRECTIVES
 
-    const whitelistRe = new RegExp(
-      whitelist.map((w) => regexp.escape(w)).join('|'),
+    const allowlistRe = new RegExp(
+      allowlist.map((w) => regexp.escape(w)).join('|'),
       'gu'
     )
 
@@ -180,7 +180,7 @@ module.exports = {
      * @param {string} str
      */
     function getBareString(str) {
-      return str.trim().replace(whitelistRe, '').trim()
+      return str.trim().replace(allowlistRe, '').trim()
     }
 
     /**
diff --git a/lib/rules/no-deprecated-destroyed-lifecycle.js b/lib/rules/no-deprecated-destroyed-lifecycle.js
new file mode 100644
index 00000000..c3556084
--- /dev/null
+++ b/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -0,0 +1,101 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)',
+      categories: ['vue3-essential'],
+      url:
+        'https://eslint.vuejs.org/rules/no-deprecated-destroyed-lifecycle.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      deprecatedDestroyed:
+        'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+      deprecatedBeforeDestroy:
+        'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+      insteadUnmounted: 'Instead, change to `unmounted`.',
+      insteadBeforeUnmount: 'Instead, change to `beforeUnmount`.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    return utils.executeOnVue(context, (obj) => {
+      const destroyed = utils.findProperty(obj, 'destroyed')
+
+      if (destroyed) {
+        context.report({
+          node: destroyed.key,
+          messageId: 'deprecatedDestroyed',
+          // I don't know if they have exactly the same function, so don't do autofix.
+          suggest: [
+            {
+              messageId: 'insteadUnmounted',
+              fix(fixer) {
+                return fix(fixer, destroyed, 'unmounted')
+              }
+            }
+          ]
+        })
+      }
+
+      const beforeDestroy = utils.findProperty(obj, 'beforeDestroy')
+      if (beforeDestroy) {
+        context.report({
+          node: beforeDestroy.key,
+          messageId: 'deprecatedBeforeDestroy',
+          // I don't know if they have exactly the same function, so don't do autofix.
+          suggest: [
+            {
+              messageId: 'insteadBeforeUnmount',
+              fix(fixer) {
+                return fix(fixer, beforeDestroy, 'beforeUnmount')
+              }
+            }
+          ]
+        })
+      }
+
+      /**
+       * @param {RuleFixer} fixer
+       * @param {Property} property
+       * @param {string} newName
+       */
+      function fix(fixer, property, newName) {
+        if (property.computed) {
+          if (
+            property.key.type === 'Literal' ||
+            property.key.type === 'TemplateLiteral'
+          ) {
+            return fixer.replaceTextRange(
+              [property.key.range[0] + 1, property.key.range[1] - 1],
+              newName
+            )
+          }
+          return null
+        }
+        if (property.shorthand) {
+          return fixer.insertTextBefore(property.key, `${newName}:`)
+        }
+        return fixer.replaceText(property.key, newName)
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js
new file mode 100644
index 00000000..6d0de9a8
--- /dev/null
+++ b/lib/rules/no-restricted-component-options.js
@@ -0,0 +1,215 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+
+/**
+ * @typedef {object} ParsedOption
+ * @property {Tester} test
+ * @property {string|undefined} [message]
+ */
+/**
+ * @typedef {object} MatchResult
+ * @property {Tester | undefined} [next]
+ * @property {boolean} [wildcard]
+ * @property {string} keyName
+ */
+/**
+ * @typedef { (name: string) => boolean } Matcher
+ * @typedef { (node: Property | SpreadElement) => (MatchResult | null) } Tester
+ */
+
+/**
+ * @param {string} str
+ * @returns {Matcher}
+ */
+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 {string | string[] | { name: string | string[], message?: string } } option
+ * @returns {ParsedOption}
+ */
+function parseOption(option) {
+  if (typeof option === 'string' || Array.isArray(option)) {
+    return parseOption({
+      name: option
+    })
+  }
+
+  /**
+   * @typedef {object} Step
+   * @property {Matcher} [test]
+   * @property {boolean} [wildcard]
+   */
+
+  /** @type {Step[]} */
+  const steps = []
+  for (const name of Array.isArray(option.name) ? option.name : [option.name]) {
+    if (name === '*') {
+      steps.push({ wildcard: true })
+    } else {
+      steps.push({ test: buildMatcher(name) })
+    }
+  }
+  const message = option.message
+
+  return {
+    test: buildTester(0),
+    message
+  }
+
+  /**
+   * @param {number} index
+   * @returns {Tester}
+   */
+  function buildTester(index) {
+    const { wildcard, test } = steps[index]
+    const next = index + 1
+    const needNext = steps.length > next
+    return (node) => {
+      /** @type {string} */
+      let keyName
+      if (wildcard) {
+        keyName = '*'
+      } else {
+        if (node.type !== 'Property') {
+          return null
+        }
+        const name = utils.getStaticPropertyName(node)
+        if (!name || !test(name)) {
+          return null
+        }
+        keyName = name
+      }
+
+      return {
+        next: needNext ? buildTester(next) : undefined,
+        wildcard,
+        keyName
+      }
+    }
+  }
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow specific component option',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-restricted-component-options.html'
+    },
+    fixable: null,
+    schema: {
+      type: 'array',
+      items: {
+        oneOf: [
+          { type: 'string' },
+          {
+            type: 'array',
+            items: {
+              type: 'string'
+            }
+          },
+          {
+            type: 'object',
+            properties: {
+              name: {
+                anyOf: [
+                  { type: 'string' },
+                  {
+                    type: 'array',
+                    items: {
+                      type: 'string'
+                    }
+                  }
+                ]
+              },
+              message: { type: 'string', minLength: 1 }
+            },
+            required: ['name'],
+            additionalProperties: false
+          }
+        ]
+      },
+      uniqueItems: true,
+      minItems: 0
+    },
+
+    messages: {
+      // eslint-disable-next-line eslint-plugin/report-message-format
+      restrictedOption: '{{message}}'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    if (!context.options || context.options.length === 0) {
+      return {}
+    }
+    /** @type {ParsedOption[]} */
+    const options = context.options.map(parseOption)
+
+    return utils.defineVueVisitor(context, {
+      onVueObjectEnter(node) {
+        for (const option of options) {
+          verify(node, option.test, option.message)
+        }
+      }
+    })
+
+    /**
+     * @param {ObjectExpression} node
+     * @param {Tester} test
+     * @param {string | undefined} customMessage
+     * @param {string[]} path
+     */
+    function verify(node, test, customMessage, path = []) {
+      for (const prop of node.properties) {
+        const result = test(prop)
+        if (!result) {
+          continue
+        }
+        if (result.next) {
+          if (
+            prop.type !== 'Property' ||
+            prop.value.type !== 'ObjectExpression'
+          ) {
+            continue
+          }
+          verify(prop.value, result.next, customMessage, [
+            ...path,
+            result.keyName
+          ])
+        } else {
+          const message =
+            customMessage || defaultMessage([...path, result.keyName])
+          context.report({
+            node: prop.type === 'Property' ? prop.key : prop,
+            messageId: 'restrictedOption',
+            data: { message }
+          })
+        }
+      }
+    }
+
+    /**
+     * @param {string[]} path
+     */
+    function defaultMessage(path) {
+      return `Using \`${path.join('.')}\` is not allowed.`
+    }
+  }
+}
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index eaf9ce57..468e718e 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -7,15 +7,24 @@
 const semver = require('semver')
 const utils = require('../utils')
 
+/**
+ * @typedef {object} SyntaxRule
+ * @property {string | ((range: semver.Range) => boolean)} supported
+ * @property { (context: RuleContext) => TemplateListener } [createTemplateBodyVisitor]
+ * @property { (context: RuleContext) => RuleListener } [createScriptVisitor]
+ */
+
 const FEATURES = {
   // Vue.js 2.5.0+
   'slot-scope-attribute': require('./syntaxes/slot-scope-attribute'),
   // Vue.js 2.6.0+
   'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
   'v-slot': require('./syntaxes/v-slot'),
-
   // >=2.6.0-beta.1 <=2.6.0-beta.3
-  'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand')
+  'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
+  // Vue.js 3.0.0+
+  'v-model-argument': require('./syntaxes/v-model-argument'),
+  'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers')
 }
 
 const cache = new Map()
@@ -77,10 +86,14 @@ module.exports = {
       forbiddenDynamicDirectiveArguments:
         'Dynamic arguments are not supported until Vue.js "2.6.0".',
       forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
-
       // >=2.6.0-beta.1 <=2.6.0-beta.3
       forbiddenVBindPropModifierShorthand:
-        '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".'
+        '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".',
+      // Vue.js 3.0.0+
+      forbiddenVModelArgument:
+        'Argument on `v-model` is not supported until Vue.js "3.0.0".',
+      forbiddenVModelCustomModifiers:
+        'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".'
     }
   },
   /** @param {RuleContext} context */
@@ -100,7 +113,7 @@ module.exports = {
 
     /**
      * Check whether a given case object is full-supported on the configured node version.
-     * @param { { supported?: string | ((range: semver.Range) => boolean) } } aCase The case object to check.
+     * @param {SyntaxRule} aCase The case object to check.
      * @returns {boolean} `true` if it's supporting.
      */
     function isNotSupportingVersion(aCase) {
@@ -110,19 +123,38 @@ module.exports = {
       return versionRange.intersects(getSemverRange(`<${aCase.supported}`))
     }
 
-    const keys = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
+    const syntaxNames = /** @type {(keyof FEATURES)[]} */ (Object.keys(
+      FEATURES
+    ))
 
-    const templateBodyVisitor = keys
-      .filter((syntaxName) => !ignores.includes(syntaxName))
-      .filter((syntaxName) => isNotSupportingVersion(FEATURES[syntaxName]))
-      .reduce((result, syntaxName) => {
-        const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context)
-        if (visitor) {
-          return utils.compositingVisitors(result, visitor)
-        }
-        return result
-      }, {})
+    /** @type {TemplateListener} */
+    let templateBodyVisitor = {}
+    /** @type {RuleListener} */
+    let scriptVisitor = {}
 
-    return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
+    for (const syntaxName of syntaxNames) {
+      /** @type {SyntaxRule} */
+      const syntax = FEATURES[syntaxName]
+      if (ignores.includes(syntaxName) || !isNotSupportingVersion(syntax)) {
+        continue
+      }
+      if (syntax.createTemplateBodyVisitor) {
+        const visitor = syntax.createTemplateBodyVisitor(context)
+        templateBodyVisitor = utils.compositingVisitors(
+          templateBodyVisitor,
+          visitor
+        )
+      }
+      if (syntax.createScriptVisitor) {
+        const visitor = syntax.createScriptVisitor(context)
+        scriptVisitor = utils.compositingVisitors(scriptVisitor, visitor)
+      }
+    }
+
+    return utils.defineTemplateBodyVisitor(
+      context,
+      templateBodyVisitor,
+      scriptVisitor
+    )
   }
 }
diff --git a/lib/rules/syntaxes/dynamic-directive-arguments.js b/lib/rules/syntaxes/dynamic-directive-arguments.js
index 314bceea..595f70fb 100644
--- a/lib/rules/syntaxes/dynamic-directive-arguments.js
+++ b/lib/rules/syntaxes/dynamic-directive-arguments.js
@@ -5,7 +5,7 @@
 'use strict'
 module.exports = {
   supported: '2.6.0',
-  /** @param {RuleContext} context */
+  /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
      * Reports dynamic argument node
diff --git a/lib/rules/syntaxes/scope-attribute.js b/lib/rules/syntaxes/scope-attribute.js
index e2b0a697..c1673c3c 100644
--- a/lib/rules/syntaxes/scope-attribute.js
+++ b/lib/rules/syntaxes/scope-attribute.js
@@ -5,7 +5,7 @@
 'use strict'
 module.exports = {
   deprecated: '2.5.0',
-  /** @param {RuleContext} context */
+  /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
      * Reports `scope` node
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index 12d6ad67..bf84361f 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -5,7 +5,7 @@
 'use strict'
 module.exports = {
   deprecated: '2.6.0',
-  /** @param {RuleContext} context */
+  /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     const sourceCode = context.getSourceCode()
 
diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js
index 6efa4155..e05be5c3 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -10,6 +10,7 @@ module.exports = {
    * @param {RuleContext} context
    * @param {object} option
    * @param {boolean} [option.fixToUpgrade]
+   * @returns {TemplateListener}
    */
   createTemplateBodyVisitor(context, { fixToUpgrade } = {}) {
     const sourceCode = context.getSourceCode()
diff --git a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
index 1374379f..4038c81a 100644
--- a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
+++ b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -12,7 +12,7 @@ module.exports = {
   supported: (versionRange) => {
     return !versionRange.intersects(unsupported)
   },
-  /** @param {RuleContext} context */
+  /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
      * Reports `.prop` shorthand node
diff --git a/lib/rules/syntaxes/v-model-argument.js b/lib/rules/syntaxes/v-model-argument.js
new file mode 100644
index 00000000..bba028e1
--- /dev/null
+++ b/lib/rules/syntaxes/v-model-argument.js
@@ -0,0 +1,23 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+  supported: '3.0.0',
+  /** @param {RuleContext} context @returns {TemplateListener} */
+  createTemplateBodyVisitor(context) {
+    return {
+      /** @param {VDirectiveKey & { argument: VExpressionContainer | VIdentifier }} node */
+      "VAttribute[directive=true] > VDirectiveKey[name.name='model'][argument!=null]"(
+        node
+      ) {
+        context.report({
+          node: node.argument,
+          messageId: 'forbiddenVModelArgument'
+        })
+      }
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/v-model-custom-modifiers.js b/lib/rules/syntaxes/v-model-custom-modifiers.js
new file mode 100644
index 00000000..5630f9ad
--- /dev/null
+++ b/lib/rules/syntaxes/v-model-custom-modifiers.js
@@ -0,0 +1,33 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
+
+module.exports = {
+  supported: '3.0.0',
+  /** @param {RuleContext} context @returns {TemplateListener} */
+  createTemplateBodyVisitor(context) {
+    return {
+      /** @param {VDirectiveKey} node */
+      "VAttribute[directive=true] > VDirectiveKey[name.name='model'][modifiers.length>0]"(
+        node
+      ) {
+        for (const modifier of node.modifiers) {
+          if (!BUILTIN_MODIFIERS.has(modifier.name)) {
+            context.report({
+              node: modifier,
+              messageId: 'forbiddenVModelCustomModifiers'
+            })
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index c3b5dfc0..9d563a2d 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -5,7 +5,7 @@
 'use strict'
 module.exports = {
   supported: '2.6.0',
-  /** @param {RuleContext} context */
+  /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     const sourceCode = context.getSourceCode()
 
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 4e1725ef..a0710a8d 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1215,6 +1215,7 @@ module.exports = {
    * Find all functions which do not always return values
    * @param {boolean} treatUndefinedAsUnspecified
    * @param { (node: ESNode) => void } cb Callback function
+   * @returns {RuleListener}
    */
   executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, cb) {
     /**
@@ -1580,23 +1581,26 @@ function defineTemplateBodyVisitor(
 }
 
 /**
- * @param {RuleListener} visitor
- * @param {...RuleListener} visitors
- * @returns {RuleListener}
+ * @template T
+ * @param {T} visitor
+ * @param {...(TemplateListener | RuleListener | NodeListener)} visitors
+ * @returns {T}
  */
 function compositingVisitors(visitor, ...visitors) {
   for (const v of visitors) {
     for (const key in v) {
+      // @ts-expect-error
       if (visitor[key]) {
+        // @ts-expect-error
         const o = visitor[key]
-        /** @param {any[]} args */
+        // @ts-expect-error
         visitor[key] = (...args) => {
-          // @ts-expect-error
           o(...args)
           // @ts-expect-error
           v[key](...args)
         }
       } else {
+        // @ts-expect-error
         visitor[key] = v[key]
       }
     }


Test patch:
diff --git a/tests/lib/rules/html-indent.js b/tests/lib/rules/html-indent.js
index d70b0395..316eb925 100644
--- a/tests/lib/rules/html-indent.js
+++ b/tests/lib/rules/html-indent.js
@@ -78,6 +78,7 @@ function loadPatterns(additionalValid, additionalInvalid) {
 
       return Object.assign({}, pattern, { code, output, errors })
     })
+    .filter((invalid) => invalid.errors.length > 0) // Empty errors cannot be verified with eslint 7.3.
     .filter(Boolean)
 
   return {
diff --git a/tests/lib/rules/no-bare-strings-in-template.js b/tests/lib/rules/no-bare-strings-in-template.js
index 3fc4a432..8856ab96 100644
--- a/tests/lib/rules/no-bare-strings-in-template.js
+++ b/tests/lib/rules/no-bare-strings-in-template.js
@@ -214,7 +214,7 @@ tester.run('no-bare-strings-in-template', rule, {
         <h1>ipsum</h1>
       </template>
       `,
-      options: [{ whitelist: ['Lorem'] }],
+      options: [{ allowlist: ['Lorem'] }],
       errors: [
         {
           line: 4,
diff --git a/tests/lib/rules/no-deprecated-destroyed-lifecycle.js b/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
new file mode 100644
index 00000000..00dda6ed
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -0,0 +1,377 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-destroyed-lifecycle')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        unmounted () {},
+        beforeUnmount () {},
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        unmounted,
+        beforeUnmount,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeUnmount,
+        unmounted,
+        errorCaptured,
+        renderTracked,
+        renderTriggered,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeUnmount:beforeDestroy,
+        unmounted:destroyed,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        ...beforeDestroy,
+        ...destroyed,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        [beforeDestroy] () {},
+        [destroyed] () {},
+      }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeDestroy () {},
+        destroyed () {},
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        beforeUnmount () {},
+        destroyed () {},
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message:
+            'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+          line: 5,
+          suggestions: [
+            {
+              desc: 'Instead, change to `unmounted`.',
+              output: `
+      <script>
+      export default {
+        beforeDestroy () {},
+        unmounted () {},
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeDestroy,
+        destroyed,
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        beforeUnmount:beforeDestroy,
+        destroyed,
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message:
+            'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+          line: 5,
+          suggestions: [
+            {
+              desc: 'Instead, change to `unmounted`.',
+              output: `
+      <script>
+      export default {
+        beforeDestroy,
+        unmounted:destroyed,
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeDestroy,
+        destroyed,
+        errorCaptured,
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 12,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeUnmount:beforeDestroy,
+        destroyed,
+        errorCaptured,
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message:
+            'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+          line: 13,
+          suggestions: [
+            {
+              desc: 'Instead, change to `unmounted`.',
+              output: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeDestroy,
+        unmounted:destroyed,
+        errorCaptured,
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        ['beforeDestroy']() {},
+        ['destroyed']() {},
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        ['beforeUnmount']() {},
+        ['destroyed']() {},
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message:
+            'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+          line: 5,
+          suggestions: [
+            {
+              desc: 'Instead, change to `unmounted`.',
+              output: `
+      <script>
+      export default {
+        ['beforeDestroy']() {},
+        ['unmounted']() {},
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        [\`beforeDestroy\`]() {},
+        [\`destroyed\`]() {},
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        [\`beforeUnmount\`]() {},
+        [\`destroyed\`]() {},
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message:
+            'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
+          line: 5,
+          suggestions: [
+            {
+              desc: 'Instead, change to `unmounted`.',
+              output: `
+      <script>
+      export default {
+        [\`beforeDestroy\`]() {},
+        [\`unmounted\`]() {},
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-restricted-component-options.js b/tests/lib/rules/no-restricted-component-options.js
new file mode 100644
index 00000000..512eb53c
--- /dev/null
+++ b/tests/lib/rules/no-restricted-component-options.js
@@ -0,0 +1,317 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-component-options')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('no-restricted-component-options', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        /* ✗ BAD */
+        init: function () {},
+        beforeCompile: function () {},
+        compiled: function () {},
+        activate: function () {},
+        ready: function () {},
+        attached: function () {},
+        detached: function () {},
+
+        /* ✓ GOOD */
+        beforeCreate: function () {},
+        activated: function () {},
+        mounted: function () {},
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          size: Number,
+          name: {
+            type: String,
+            required: true,
+            /* ✗ BAD */
+            twoWay: true
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        /* ✓ GOOD */
+        beforeCreate: function () {},
+        activated: function () {},
+        mounted: function () {},
+      }
+      </script>
+      `,
+      options: [
+        'init',
+        'beforeCompile',
+        'compiled',
+        'activate',
+        'ready',
+        '/^(?:at|de)tached$/'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        foo: {
+          ...bar,
+          baz
+        }
+      }
+      </script>
+      `,
+      options: [['foo', '*', 'baz']]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        foo: {
+          ...bar,
+          baz
+        }
+      }
+      </script>
+      `,
+      options: [['foo', 'bar']]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        /* ✗ BAD */
+        init: function () {},
+        beforeCompile: function () {},
+        compiled: function () {},
+        activate: function () {},
+        ready: function () {},
+        attached: function () {},
+        detached: function () {},
+
+        /* ✓ GOOD */
+        beforeCreate: function () {},
+        activated: function () {},
+        mounted: function () {},
+      }
+      </script>
+      `,
+      options: [
+        'init',
+        'beforeCompile',
+        'compiled',
+        'activate',
+        'ready',
+        '/^(?:at|de)tached$/'
+      ],
+      errors: [
+        {
+          message: 'Using `init` is not allowed.',
+          line: 5,
+          column: 9
+        },
+        {
+          message: 'Using `beforeCompile` is not allowed.',
+          line: 6,
+          column: 9
+        },
+        {
+          message: 'Using `compiled` is not allowed.',
+          line: 7,
+          column: 9
+        },
+        {
+          message: 'Using `activate` is not allowed.',
+          line: 8,
+          column: 9
+        },
+        {
+          message: 'Using `ready` is not allowed.',
+          line: 9,
+          column: 9
+        },
+        {
+          message: 'Using `attached` is not allowed.',
+          line: 10,
+          column: 9
+        },
+        {
+          message: 'Using `detached` is not allowed.',
+          line: 11,
+          column: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          size: Number,
+          name: {
+            type: String,
+            required: true,
+            /* ✗ BAD */
+            twoWay: true
+          }
+        }
+      }
+      </script>
+      `,
+      options: [['props', '/.*/', 'twoWay']],
+      errors: [
+        {
+          message: 'Using `props.name.twoWay` is not allowed.',
+          line: 10
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          size: Number,
+          name: {
+            type: String,
+            required: true,
+            /* ✗ BAD */
+            twoWay: true
+          }
+        },
+        init: function () {},
+        beforeCompile: function () {},
+        compiled: function () {},
+        activate: function () {},
+        ready: function () {},
+        attached: function () {},
+        detached: function () {},
+
+        beforeCreate: function () {},
+        activated: function () {},
+        mounted: function () {},
+      }
+      </script>
+      `,
+      options: [
+        {
+          name: 'init',
+          message: 'Use "beforeCreate" instead.'
+        },
+        {
+          name: '/^(?:at|de)tached$/',
+          message: '"attached" and "detached" is deprecated.'
+        },
+        {
+          name: ['props', '/.*/', 'twoWay'],
+          message: '"props.*.twoWay" cannot be used.'
+        }
+      ],
+      errors: [
+        {
+          message: '"props.*.twoWay" cannot be used.',
+          line: 10,
+          column: 13
+        },
+        {
+          message: 'Use "beforeCreate" instead.',
+          line: 13,
+          column: 9
+        },
+        {
+          message: '"attached" and "detached" is deprecated.',
+          line: 18,
+          column: 9
+        },
+        {
+          message: '"attached" and "detached" is deprecated.',
+          line: 19,
+          column: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          size: Number,
+          [name]: {
+            type: String,
+            required: true,
+            /* ✗ BAD */
+            twoWay: true
+          }
+        }
+      }
+      </script>
+      `,
+      options: [['props', '*', 'twoWay']],
+      errors: [
+        {
+          message: 'Using `props.*.twoWay` is not allowed.',
+          line: 10
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        foo: {
+          ...bar
+        }
+      }
+      </script>
+      `,
+      options: [['foo', '*']],
+      errors: [
+        {
+          message: 'Using `foo.*` is not allowed.',
+          line: 5
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unsupported-features/v-model-argument.js b/tests/lib/rules/no-unsupported-features/v-model-argument.js
new file mode 100644
index 00000000..ceb79784
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/v-model-argument.js
@@ -0,0 +1,59 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('v-model-argument', '^2.6.0')
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-unsupported-features/v-model-argument', rule, {
+  valid: [
+    {
+      code: `
+      <template>
+        <MyInput v-model:foo="foo" />
+      </template>`,
+      options: buildOptions({ version: '^3.0.0' })
+    },
+    {
+      code: `
+      <template>
+        <MyInput v-model="foo" />
+      </template>`,
+      options: buildOptions()
+    },
+    {
+      code: `
+      <template>
+        <MyInput v-bind:foo.sync="foo" />
+      </template>`,
+      options: buildOptions()
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <MyInput v-model:foo="foo" />
+      </template>`,
+      options: buildOptions(),
+      errors: [
+        {
+          message:
+            'Argument on `v-model` is not supported until Vue.js "3.0.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js b/tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js
new file mode 100644
index 00000000..52127273
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js
@@ -0,0 +1,73 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('v-model-custom-modifiers', '^2.6.0')
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-unsupported-features/v-model-custom-modifiers', rule, {
+  valid: [
+    {
+      code: `
+      <template>
+        <MyInput v-model:foo.bar="foo" />
+      </template>`,
+      options: buildOptions({ version: '^3.0.0' })
+    },
+    {
+      code: `
+      <template>
+        <MyInput v-model.foo="foo" />
+      </template>`,
+      options: buildOptions({ version: '^3.0.0' })
+    },
+    {
+      code: `
+      <template>
+        <MyInput v-model.trim="foo" />
+      </template>`,
+      options: buildOptions()
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <MyInput v-model:foo.bar="foo" />
+      </template>`,
+      options: buildOptions(),
+      errors: [
+        {
+          message:
+            'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <MyInput v-model.foo="foo" />
+      </template>`,
+      options: buildOptions(),
+      errors: [
+        {
+          message:
+            'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/script-indent.js b/tests/lib/rules/script-indent.js
index 42497d2d..865e7314 100644
--- a/tests/lib/rules/script-indent.js
+++ b/tests/lib/rules/script-indent.js
@@ -79,6 +79,7 @@ function loadPatterns(additionalValid, additionalInvalid) {
 
       return Object.assign({}, pattern, { code, output, errors })
     })
+    .filter((invalid) => invalid.errors.length > 0) // Empty errors cannot be verified with eslint 7.3.
     .filter(Boolean)
 
   return {