Repository: vuejs/eslint-plugin-vue
Pull request #1212 (2020-06-15 03:37:28)

Add `"v-model-argument"` and `"v-model-custom-modifiers"` to the syntax checked by the `vue/no-unsupported-features` rule.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 1fe65868..16e0ef98 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+) |  |
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-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..7eda5423 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'),
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-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-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 {